/*
 * $Id: meta.c 520 2008-05-14 08:43:58Z jasta00 $
 *
 * Copyright (C) 2008 Josh Guilfoyle <jasta@devtcg.org>
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2, or (at your option) any
 * later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 */

#include "main.h"

#include <gconf/gconf-client.h>

#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

#include <sqlite3.h>

#include <tag_c.h>

#include <libsoup/soup.h>
#include <libsoup/soup-message.h>

#include <wand/magick-wand.h>

#include "inotify.h"

#include "db.h"
#include "utils.h"
#include "meta.h"

/*****************************************************************************/

/* List of folders to watch/crawl for media. */
static GSList *watch_folders = NULL;
#define TEMPORARY_DEFAULT_FOLDER "/home/jguilfoyle/Music"

/*****************************************************************************/

/* Thumbnail target size. */
#define TARGET_W 64
#define TARGET_H 64

enum worker_type
{
	WORKER_ARTIST,
	WORKER_ALBUM
};

/*
 * Abstract data to schedule a download worker to get artist and album
 * photos/artwork.
 */
struct worker
{
	enum worker_type type;
	sqlite_int64 id;
	char *name;
	char *extra;                       /* artist name if type == ALBUM */

	/* Context while parsing. */
	int context;
	char *mbid;
	char *imgurl;
	gboolean bighack;
};

/*
 * TODO: This is a very inefficient model we've come up with.  Improve
 * some day...
 */
static GThreadPool *workers = NULL;

/*
 * Use a single session container for every worker thread.
 */
static SoupSession *http_cli;

enum queuedwork_type
{
	/* Scheduled by the worker threads that download artwork and correlate
	 * Last.fm data. */
	QUEUED_IMG,
	QUEUED_MBID,

	/* Scheduled by the inotify watchers when we are busy crawling and thus
	 * cannot safely update the database directly. */
	QUEUED_WATCH_CREATE_DIR,
	QUEUED_WATCH_MOVE,
	QUEUED_WATCH_MODIFY,
	QUEUED_WATCH_DELETE,
};

struct queuedwork
{
	enum queuedwork_type type;
	void *work;
};

/*
 * Holds an entity to pass to the main thread through `workqueue'.
 */
struct queuedimg
{
	enum worker_type type;
	sqlite_int64 id;
	unsigned char *data;
	size_t len;
};

struct queuedmbid
{
	enum worker_type type;
	sqlite_int64 id;
	char *mbid;
};

struct queuedwatch
{
	char *path1;                       /* for modify/delete */
	char *path2;                       /* used only for move */
};

/*
 * This is extern'd to other areas of the project to synchronize behaviour when
 * a crawl (or inotify callback) is busy.
 */
GStaticMutex crawling = G_STATIC_MUTEX_INIT;

/*
 * sqlite3's thread safety is not sufficient for us, so we must have a queue
 * to update the artist photo or album artwork into an existing row using
 * this queue to communicate with the main scanning thread.
 */
static GAsyncQueue *workqueue = NULL;

/*
 * Enable if you wish to detect and ignore hardlinks and
 * symlinks pointing to files already present in the normal sync path.
 */
#define TRACK_INODES

#ifdef TRACK_INODES
struct unique_inode
{
	ino_t inode;
	dev_t dev;
};

static GHashTable *inodes = NULL;
#endif /* TRACK_INODES */

/* Bleh. */
#define USE_INOTIFY

#ifdef USE_INOTIFY
static GIOChannel *inotify = NULL;
#endif /* USE_INOTIFY */

/* Though these are part of the inotify support, I envision some day
 * generalizing "watching", and thus these would remain. */
static void watch_create_dir_cb(const char *path);
static void watch_create_dir_work(sqlite3 *dbh, const char *path, gboolean needinit);
static void watch_move_cb(const char *src, const char *dst);
static void watch_move_work(sqlite3 *dbh, const char *src, const char *dst);
static void watch_modify_cb(const char *path);
static void watch_modify_work(sqlite3 *dbh, const char *path, gboolean needinit);
static void watch_delete_cb(const char *path);
static void watch_delete_work(sqlite3 *dbh, const char *path);

/*****************************************************************************/

static struct worker *worker_new(enum worker_type type, sqlite_int64 id,
  const char *name, const char *extra)
{
	struct worker *w;

	w = g_new(struct worker, 1);
	assert(w != NULL);

	w->type = type;
	w->id = id;
	w->name = g_strdup(name);
	w->extra = g_strdup(extra);
	w->mbid = NULL;
	w->imgurl = NULL;
	w->context = 0;
	w->bighack = FALSE;
}

static void worker_free(struct worker *w)
{
	assert(w != NULL);

	g_free(w->name);
	g_free(w->extra);
	g_free(w->imgurl);
	g_free(w->mbid);
	g_free(w);
}

static struct queuedwork *queuedwork_new(enum queuedwork_type type,
  gpointer data)
{
	struct queuedwork *qw;

	qw = g_new(struct queuedwork, 1);
	assert(qw != NULL);

	qw->type = type;
	qw->work = data;

	return qw;
}

static void queuedwork_free(struct queuedwork *qw)
{
	g_free(qw);
}

static struct queuedwork *queuedimg_new(enum worker_type type, sqlite_int64 id,
  const char *data, size_t len)
{
	struct queuedimg *q;

	q = g_new(struct queuedimg, 1);
	assert(q != NULL);

	q->type = type;
	q->id = id;
	q->data = g_memdup(data, len);
	q->len = len;

	return queuedwork_new(QUEUED_IMG, q);
}

static void queuedimg_free(struct queuedimg *q)
{
	g_free(q->data);
	g_free(q);
}

static struct queuedwork *queuedmbid_new(enum worker_type type, sqlite_int64 id,
  const char *mbid)
{
	struct queuedmbid *q;

	q = g_new(struct queuedmbid, 1);
	assert(q != NULL);

	q->type = type;
	q->id = id;
	q->mbid = g_strdup(mbid);

	return queuedwork_new(QUEUED_MBID, q);
}

static void queuedmbid_free(struct queuedmbid *q)
{
	g_free(q->mbid);
	g_free(q);
}

static struct queuedwork *queuedwatch_new(enum queuedwork_type type,
  const char *path1, const char *path2)
{
	struct queuedwatch *q;

	assert(path1 != NULL);

	q = g_new(struct queuedwatch, 1);
	assert(q != NULL);

	q->path1 = g_strdup(path1);

	if (path2 != NULL)
		q->path2 = g_strdup(path2);
	else
		q->path2 = NULL;

	return queuedwork_new(type, q);
}

static void queuedwatch_free(struct queuedwatch *q)
{
	g_free(q->path1);

	if (q->path2 != NULL)
		g_free(q->path2);

	g_free(q);
}

/*****************************************************************************/

static void artist_similar_start(GMarkupParseContext *ctx, const char *name,
  const char **anames, const char **avalues,
  gpointer udata, GError **err)
{
	struct worker *w = udata;
	int i;

	if (w->context > 0)
		return;

	/* The root tag is <similarartists ... picture="http://..."> */
	for (i = 0; anames[i] != NULL; i++)
	{
		if (strcmp(anames[i], "picture") == 0)
		{
			w->imgurl = g_strdup(avalues[i]);
			break;
		}
	}

	w->context = 1;
}

static GMarkupParser artist_similar_parser =
{
	start_element: artist_similar_start,
};

static void compute_thumb_size(MagickWand *wand,
  unsigned long tw, unsigned long th, unsigned long *rw, unsigned long *rh)
{
	unsigned long w, h;

	w = MagickGetImageWidth(wand);
	h = MagickGetImageHeight(wand);

	if (w < h)
	{
		*rw = tw;
		*rh = (int)(((double)h / ((double)w / (double)tw)));
	}
	else
	{
		*rh = th;
		*rw = (int)(((double)w / ((double)h / (double)th)));
	}
}

static gboolean thumbnail_image(const char *url,
  const unsigned char *img, size_t imglen,
  unsigned long w, unsigned long h,
  unsigned char **thumbnail, size_t *thumbnaillen)
{
	MagickWand *wand;
	MagickBooleanType rc;
	char *err;
	ExceptionType severity;
	unsigned long tw, th;

	wand = NewMagickWand();
	assert(wand != NULL);

	if ((rc = MagickReadImageBlob(wand, img, imglen)) == MagickFalse)
		goto magick_error;

	compute_thumb_size(wand, w, h, &tw, &th);

	if ((rc = MagickThumbnailImage(wand, tw, th)) == MagickFalse)
		goto magick_error;

	if (tw != th)
	{
		long x, y;

		if (tw > th)
		{
			x = (tw - w) / 2;
			y = 0;
		}
		else
		{
			x = 0;
			y = (th - h) / 2;
		}

		if ((rc = MagickCropImage(wand, w, h, x, y)) == MagickFalse)
			goto magick_error;
	}

	MagickSetFormat(wand, "JPEG");

	*thumbnail = MagickGetImageBlob(wand, thumbnaillen);
	assert(*thumbnail != NULL);
	assert(*thumbnaillen > 0);

	DestroyMagickWand(wand);

	return TRUE;

magick_error:
	DestroyMagickWand(wand);

	err = MagickGetException(wand, &severity);
	assert(err != NULL);

	g_error("%s: %s %s %lu %s", GetMagickModule(), err);
	MagickRelinquishMemory(err);

	return FALSE;
}

static void download_and_store(SoupSession *http, struct worker *w)
{
	SoupMessage *msg;
	guint status;

	assert(w != NULL);
	assert(w->imgurl != NULL);

	//g_message("Woo, downloading image: (_id=%u) %s", (unsigned int)w->id, w->imgurl);

	msg = soup_message_new("GET", w->imgurl);
	assert(msg != NULL);

	status = soup_session_send_message(http, msg);

	if (SOUP_STATUS_IS_SUCCESSFUL(status) == TRUE &&
	    msg->response.length > 0)
	{
		struct queuedwork *img;
		gboolean rc;
		unsigned char *thumbnail;
		size_t thumbnaillen;

		if (w->bighack == TRUE)
		{
#if 0
			rc = TRUE;
			thumbnail = msg->response.body;
			thumbnaillen = msg->response.length;
#endif
			rc = thumbnail_image(w->imgurl,
			  msg->response.body, msg->response.length,
			  300, 300,
			  &thumbnail, &thumbnaillen);
		}
		else
		{
			rc = thumbnail_image(w->imgurl,
			  msg->response.body, msg->response.length,
			  TARGET_W, TARGET_H,
			  &thumbnail, &thumbnaillen);
		}

		if (rc == TRUE)
		{
			img = queuedimg_new(w->type, w->id,
			  thumbnail, thumbnaillen);
			assert(img != NULL);

			if (w->bighack == FALSE)
				MagickRelinquishMemory(thumbnail);

			g_async_queue_push(workqueue, img);
		}
		else
		{
			g_warning("Couldn't generate thumbnail for %s...", w->imgurl);
		}
	}

	g_object_unref(msg);
}

static void store_mbid(struct worker *w)
{
	struct queuedwork *qw;

	qw = queuedmbid_new(w->type, w->id, w->mbid);
	assert(qw != NULL);

	g_async_queue_push(workqueue, qw);
}

static void download_artist_photo(SoupSession *http, struct worker *w)
{
	SoupMessage *msg;
	gchar *url;
	gchar *artist;
	guint status;

	artist = soup_uri_encode(w->name, "&'()");
	assert(artist != NULL);

	url = g_strdup_printf("http://ws.audioscrobbler.com/1.0/artist/%s/similar.xml", artist);
	assert(url != NULL);

	g_free(artist);

	//g_message("get %s...", url);

	msg = soup_message_new("GET", url);
	assert(msg != NULL);

	status = soup_session_send_message(http, msg);
	g_message("Got %s HTTP %u, %u bytes!", url, status, msg->response.length);

	if (SOUP_STATUS_IS_SUCCESSFUL(status) == TRUE)
	{
		GMarkupParseContext *parser;
		GError *err = NULL;
		gboolean parsed;

		parser = g_markup_parse_context_new(&artist_similar_parser,
		  0, w, NULL);

		parsed = g_markup_parse_context_parse(parser,
		  msg->response.body, msg->response.length, &err);

		if (parsed == TRUE)
			parsed = g_markup_parse_context_end_parse(parser, &err);

		if (parsed == FALSE)
		{
			g_critical("[%s]: failed to parse: %s\n", url, err->message);
			g_error_free(err);
		}

		g_markup_parse_context_free(parser);

		if (w->imgurl != NULL)
			download_and_store(http, w);

		if (w->mbid != NULL)
			store_mbid(w);
	}

	g_object_unref(msg);

	g_free(url);
}

#define ALBUM_INFO_CTX_INITIAL (0)
#define ALBUM_INFO_CTX_COVERART (1 << 1)
#define ALBUM_INFO_CTX_MBID (1 << 3)
#define ALBUM_INFO_CTX_DONT_CARE (1 << 2)

static void album_info_start(GMarkupParseContext *ctx, const char *name,
  const char **anames, const char **avalues,
  gpointer udata, GError **err)
{
	struct worker *w = udata;

	if (w->context == ALBUM_INFO_CTX_DONT_CARE)
		return;

	if (strcmp(name, "coverart") == 0)
		w->context = ALBUM_INFO_CTX_COVERART;
	else if (strcmp(name, "mbid") == 0)
	{
		/* TODO: Check that parent is <album> and not <track>. */
		w->context = ALBUM_INFO_CTX_MBID;
	}
}

static void album_info_end(GMarkupParseContext *ctx, const char *name,
  gpointer udata, GError **err)
{
	struct worker *w = udata;

	if (w->context == ALBUM_INFO_CTX_DONT_CARE)
		return;

	if (w->imgurl != NULL && w->mbid != NULL)
		w->context = ALBUM_INFO_CTX_DONT_CARE;
	else if (strcmp(name, "coverart") == 0)
		w->context = ALBUM_INFO_CTX_INITIAL;
	else if (strcmp(name, "mbid") == 0)
		w->context = ALBUM_INFO_CTX_INITIAL;
}

static void album_info_text(GMarkupParseContext *ctx, const char *text,
  gsize textlen, gpointer udata, GError **err)
{
	struct worker *w = udata;

	if (w->context == ALBUM_INFO_CTX_DONT_CARE)
		return;

	if (w->context == ALBUM_INFO_CTX_COVERART && w->imgurl == NULL)
	{
		if (strncmp(text, "http://", 7) != 0)
			return;

		w->imgurl = g_strndup(text, textlen);
	}
	else if (w->context == ALBUM_INFO_CTX_MBID && w->mbid == NULL)
	{
		if (isalnum(text[0]))
			w->mbid = g_strndup(text, textlen);
	}
}

static GMarkupParser album_info_parser =
{
	start_element: album_info_start,
	end_element: album_info_end,
	text: album_info_text,
};

/*
 * Simple hack to try to analyze the cover image URL we were served from
 * Last.fm.  The <small>, <medium>, and <large> cover artwork is all medium
 * size, despite there being simple hacks that can access the larger versions.
 */
static gboolean big_artwork_heuristic(struct worker *w)
{
	assert(w->type == WORKER_ALBUM);
	assert(w->imgurl != NULL);

	if (strstr(w->imgurl, "amazon.com") != NULL)
	{
		char *med;

		if ((med = strstr(w->imgurl, "MZZZZZZZ")) == NULL)
			return FALSE;

		strncpy(med, "L", 1);
		w->bighack = TRUE;
	}
	else if (strstr(w->imgurl, "last.fm") != NULL)
	{
		char *dim;

		if ((dim = strstr(w->imgurl, "130x130")) == NULL)
			return FALSE;

		strncpy(dim, "300x300", 7);
		w->bighack = TRUE;
	}
	else if (strstr(w->imgurl, "7digital.com") != NULL)
	{
		char *dim;

		if ((dim = strstr(w->imgurl, "_182.")) == NULL)
			return FALSE;

		strncpy(dim + 1, "350", 3);
		w->bighack = TRUE;
	}

	return TRUE;
}

static void download_album_artwork(SoupSession *http, struct worker *w)
{
	SoupMessage *msg;
	gchar *url;
	gchar *artist, *album;
	guint status;

	artist = soup_uri_encode(w->extra, "&'()");
	assert(artist != NULL);

	album = soup_uri_encode(w->name, "&'()");
	assert(album != NULL);

	url = g_strdup_printf("http://ws.audioscrobbler.com/1.0/album/%s/%s/info.xml", artist, album);
	assert(url != NULL);

	g_free(album);
	g_free(artist);

	//g_message("GET %s...", url);

	msg = soup_message_new("GET", url);
	assert(msg != NULL);

	status = soup_session_send_message(http, msg);
	g_message("Got %s HTTP %u, %u bytes!", url, status, msg->response.length);

	if (SOUP_STATUS_IS_SUCCESSFUL(status) == TRUE)
	{
		GMarkupParseContext *parser;
		GError *err = NULL;
		gboolean parsed;

		parser = g_markup_parse_context_new(&album_info_parser,
		  0, w, NULL);

		parsed = g_markup_parse_context_parse(parser,
		  msg->response.body, msg->response.length, &err);

		if (parsed == TRUE)
			parsed = g_markup_parse_context_end_parse(parser, &err);

		if (parsed == FALSE)
		{
			g_critical("[%s]: failed to parse: %s\n", url, err->message);
			g_error_free(err);
		}

		g_markup_parse_context_free(parser);

		/* Did we actually find a coverart image? */
		if (w->imgurl != NULL)
		{
			/* XXX: Hack to get big cover images. */
			if (big_artwork_heuristic(w) == TRUE)
				/*g_warning("Attempting big artwork hack: %s!\n", w->imgurl)*/;

			download_and_store(http, w);
		}

		if (w->mbid != NULL)
			store_mbid(w);
	}

	g_object_unref(msg);

	g_free(url);
}

static void download_artwork(gpointer data, gpointer udata)
{
//	SoupSession *http;
	struct worker *w = data;

	assert(w != NULL);

//	http = soup_session_sync_new();

	switch (w->type)
	{
		case WORKER_ARTIST:
			download_artist_photo(http_cli, w);
			break;

		case WORKER_ALBUM:
			download_album_artwork(http_cli, w);
			break;

		default:
			abort();
			break;
	}

//	g_object_unref(http);

	worker_free(w);
}

/*****************************************************************************/

static void set_watch_folders(GSList *folders)
{
	/* Destroy the existing watch_folders entry. */
	while (watch_folders != NULL)
	{
		g_free(watch_folders->data);
		watch_folders = g_slist_delete_link(watch_folders, watch_folders);
	}

	watch_folders = folders;
}

static void watch_folders_changed_cb(GConfClient *client,
  guint cnxn_id, GConfEntry *entry, gpointer udata)
{
	GConfValue *value = gconf_entry_get_value(entry);

	if (value == NULL || value->type != GCONF_VALUE_LIST)
		set_watch_folders(NULL);
	else
	{
		assert(gconf_value_get_list_type(value) == GCONF_VALUE_STRING);
		set_watch_folders(gconf_value_get_list(value));
	}

	/* TODO: Force meta crawl to restart if it's already crawling. */
	meta_crawl();
}

gboolean meta_init(void)
{
	GError *err = NULL;
	GSList *list;

	/* Set the tag library to not manage allocations for us.  We will need
	 * this to manage those allocations indirectly through SQLite. */
	taglib_set_string_management_enabled(FALSE);

#ifdef USE_INOTIFY
	inotify = my_inotify_open();
	assert(inotify != NULL);

	my_inotify_wrapper(inotify, watch_create_dir_cb,
	  watch_move_cb, watch_modify_cb, watch_delete_cb);
#endif /* USE_INOTIFY */

	list = gconf_client_get_list(get_gconf_client(),
	  "/apps/five/watch_folders", GCONF_VALUE_STRING, &err);

	if (list == NULL)
	{
		if (err == NULL)
		{
			list = g_slist_prepend(list, g_strdup(TEMPORARY_DEFAULT_FOLDER));
			gconf_client_set_list(get_gconf_client(),
			  "/apps/five/watch_folders", GCONF_VALUE_STRING, list, NULL);
		}
		else
		{
			g_critical("gconf_client_get_list(/apps/five/watch_folders): %s",
			  err->message);
			g_error_free(err);
		}
	}

	set_watch_folders(list);

	gconf_client_add_dir(get_gconf_client(),
	  "/apps/five", GCONF_CLIENT_PRELOAD_NONE, NULL);

	gconf_client_notify_add(get_gconf_client(),
	  "/apps/five/watch_folders", watch_folders_changed_cb,
	  NULL, NULL, NULL);

	return TRUE;
}

void meta_finish(void)
{
#ifdef USE_INOTIFY
	my_inotify_close(inotify);
#endif /* USE_INOTIFY */
}

static char *get_name_match(const char *name)
{
	char *down;
	char *and;

	if (g_ascii_strncasecmp(name, "The ", 4) == 0)
		name += 4;
	else if (g_ascii_strncasecmp(name, "A ", 2) == 0)
		name += 2;

	down = g_ascii_strdown(name, -1);

	if ((and = strstr(down, " and ")) != NULL)
	{
		size_t namelen;

		namelen = strlen(down);

		do
		{
			strcpy(and + 1, "& ");
			g_memmove(and + 3, and + 5, namelen - (and - down) - 4);
			namelen -= 2;
		}
		while ((and = strstr(and + 3, " and ")) != NULL);
	}

	return down;
}

static gboolean get_artist(sqlite3 *dbh, const char *artist, sqlite_int64 *id,
  time_t now)
{
	char *artistlow;
	sqlite3_stmt *stmt;
	int rc;

	assert(artist != NULL);
	assert(id != NULL);

	rc = sqlite3_prepare(dbh, "SELECT _id, pending_delete FROM artists WHERE name_match = ?;",
	  -1, &stmt, NULL);
	assert(rc == SQLITE_OK);

	artistlow = get_name_match(artist);
	assert(artistlow != NULL);

	sqlite3_bind_text(stmt, 1, artistlow, -1, SQLITE_STATIC);

	rc = sqlite3_step(stmt);

	if (rc != SQLITE_ROW)
	{
		sqlite3_finalize(stmt);

		rc = sqlite3_prepare(dbh, "INSERT INTO artists (name, name_match, discovery_date) VALUES (?, ?, ?);",
		  -1, &stmt, NULL);
		assert(rc == SQLITE_OK);

		sqlite3_bind_text(stmt, 1, artist, -1, SQLITE_STATIC);
		sqlite3_bind_text(stmt, 2, artistlow, -1, g_free);
		sqlite3_bind_int64(stmt, 3, (sqlite_int64)now);

		if ((rc = sqlite3_step(stmt)) != SQLITE_DONE)
		{
			g_warning("Couldn't insert artist '%s': %s\n",
			  artist, sqlite3_errmsg(dbh));
			sqlite3_finalize(stmt);
			return FALSE;
		}

		sqlite3_finalize(stmt);
		*id = sqlite3_last_insert_rowid(dbh);

		g_thread_pool_push(workers, worker_new(WORKER_ARTIST, *id, artist, NULL), NULL);
	}
	else
	{
		int pending;

		g_free(artistlow);

		*id = sqlite3_column_int64(stmt, 0);
		pending = sqlite3_column_int(stmt, 1);
		sqlite3_finalize(stmt);

		if (pending)
		{
			rc = sqlite3_prepare(dbh, "UPDATE artists SET pending_delete = 0, discovery_date = ? WHERE _id = ?", -1, &stmt, NULL);
			assert(rc == SQLITE_OK);

			sqlite3_bind_int64(stmt, 1, (sqlite_int64)now);
			sqlite3_bind_int64(stmt, 2, *id);

			rc = sqlite3_step(stmt);
			sqlite3_finalize(stmt);

			if (rc != SQLITE_DONE)
			{
				g_warning("Couldn't undelete artist '%s': %s\n",
				  artist, sqlite3_errmsg(dbh));
				return FALSE;
			}
		}
	}

	return TRUE;
}

static gboolean get_album(sqlite3 *dbh, const char *album,
  const char *artist, sqlite_int64 artist_id,
  sqlite_int64 *id, time_t now)
{
	char *albumlow;
	sqlite3_stmt *stmt;
	int rc;

	assert(album != NULL);
	assert(id != NULL);

	rc = sqlite3_prepare(dbh, "SELECT _id, pending_delete FROM albums WHERE name_match = ? AND artist_id = ?;",
	  -1, &stmt, NULL);
	assert(rc == SQLITE_OK);

	albumlow = get_name_match(album);
	assert(albumlow != NULL);

	sqlite3_bind_text(stmt, 1, albumlow, -1, SQLITE_STATIC);
	sqlite3_bind_int64(stmt, 2, artist_id);

	rc = sqlite3_step(stmt);

	if (rc != SQLITE_ROW)
	{
		sqlite3_finalize(stmt);

		rc = sqlite3_prepare(dbh, "INSERT INTO albums (name, name_match, artist_id, discovery_date) VALUES (?, ?, ?, ?);",
		  -1, &stmt, NULL);
		assert(rc == SQLITE_OK);

		sqlite3_bind_text(stmt, 1, album, -1, SQLITE_STATIC);
		sqlite3_bind_text(stmt, 2, albumlow, -1, g_free);
		sqlite3_bind_int64(stmt, 3, artist_id);
		sqlite3_bind_int64(stmt, 4, (sqlite_int64)now);

		if ((rc = sqlite3_step(stmt)) != SQLITE_DONE)
		{
			g_warning("Couldn't insert album '%s': %s\n",
			  album, sqlite3_errmsg(dbh));
			sqlite3_finalize(stmt);
			return FALSE;
		}

		sqlite3_finalize(stmt);
		*id = sqlite3_last_insert_rowid(dbh);

		g_thread_pool_push(workers, worker_new(WORKER_ALBUM, *id, album, artist), NULL);
	}
	else
	{
		int pending;

		g_free(albumlow);

		*id = sqlite3_column_int64(stmt, 0);
		pending = sqlite3_column_int(stmt, 1);
		sqlite3_finalize(stmt);

		if (pending)
		{
			rc = sqlite3_prepare(dbh, "UPDATE albums SET pending_delete = 0, discovery_date = ? WHERE _id = ?", -1, &stmt, NULL);
			assert(rc == SQLITE_OK);

			sqlite3_bind_int64(stmt, 1, (sqlite_int64)now);
			sqlite3_bind_int64(stmt, 2, *id);

			if ((rc = sqlite3_step(stmt)) != SQLITE_DONE)
			{
				g_warning("Couldn't undelete album '%s': %s\n",
				  album, sqlite3_errmsg(dbh));
				sqlite3_finalize(stmt);
				return FALSE;
			}

			sqlite3_finalize(stmt);
		}
	}

	return TRUE;
}

static gboolean insert_or_replace_song(sqlite3 *dbh,
  sqlite_int64 _id, gboolean pending, const char *path, struct stat *stbuf,
  sqlite_int64 artist_id, sqlite_int64 album_id, const char *title,
  unsigned int length, int track, time_t now)
{
	sqlite3_stmt *stmt;
	const char *sql;
	int rc;

	if (_id >= 0)
	{
		if (pending == TRUE)
		{
			rc = sqlite3_prepare(dbh, "UPDATE songs SET pending_delete = 0 WHERE _id = ?",
			  -1, &stmt, NULL);
			assert(rc == SQLITE_OK);

			sqlite3_bind_int64(stmt, 1, _id);

			if ((rc = sqlite3_step(stmt)) != SQLITE_DONE)
			{
				sqlite3_finalize(stmt);
				return FALSE;
			}

			sqlite3_finalize(stmt);
		}

		sql =
		  "UPDATE songs "
		  "   SET title = ?, "
		  "       artist_id = ?, "
		  "       album_id = ?, "
		  "       length = ?, "
		  "       track = ?, "
		  "       discovery_date = ?, "
		  "       mtime = ?, "
		  "       size = ?, "
		  "       tick = 1 ";

		/* Triggers don't support this yet. */
		g_warning("TODO: Modifying songs is not fully supported, expect unusual results!");
	}
	else
	{
		sql =
		  "INSERT INTO songs "
		  "	(title, artist_id, album_id, length, track, discovery_date, mtime, size, tick, filename) "
		  "VALUES "
		  "	(?, ?, ?, ?, ?, ?, ?, ?, 1, ?)";
	}

	rc = sqlite3_prepare(dbh, sql, -1, &stmt, NULL);
	assert(rc == SQLITE_OK);

	sqlite3_bind_text(stmt, 1, title, -1, SQLITE_STATIC);

	if (artist_id >= 0)
	{
		sqlite3_bind_int64(stmt, 2, artist_id);

		if (album_id >= 0)
			sqlite3_bind_int64(stmt, 3, album_id);
	}

	sqlite3_bind_int(stmt, 4, length);

	if (track > 0)
		sqlite3_bind_int(stmt, 5, track);

	sqlite3_bind_int64(stmt, 6, (sqlite_int64)now);
	sqlite3_bind_int64(stmt, 7, (sqlite_int64)stbuf->st_mtime);

	sqlite3_bind_int64(stmt, 8, (sqlite_int64)stbuf->st_size);

	if (_id < 0)
		sqlite3_bind_text(stmt, 9, path, -1, SQLITE_STATIC);

	rc = sqlite3_step(stmt);
	assert(rc == SQLITE_DONE);

	sqlite3_finalize(stmt);

	return TRUE;
}

static gboolean is_empty_tag(const char *tag)
{
	if (tag == NULL)
		return TRUE;

	for (; *tag != '\0'; tag++)
	{
		if (*tag != ' ' && *tag != '\t')
			return FALSE;
	}

	return TRUE;
}

/* Currently, we only support MP3 and OGG.  Eventually we should support
 * more formats and transcode to Ogg for Android to play. */
static gboolean is_supported(const char *path)
{
	size_t len = strlen(path);

	/* The extension couldn't possibly be .mp3 or .ogg. */
	if (len <= 4)
		return FALSE;

	if (g_ascii_strcasecmp(path + len - 4, ".mp3") == 0)
		return TRUE;

	if (g_ascii_strcasecmp(path + len - 4, ".ogg") == 0)
		return TRUE;

	return FALSE;
}

static gboolean handle_file(sqlite3 *dbh, const char *path, struct stat *stbuf)
{
	TagLib_File *tfile;
	TagLib_Tag *ttag;
	const TagLib_AudioProperties *taudio;
	char *artist;
	char *album;
	char *title;
	int track;
	int length;
	sqlite_int64 artist_id = -1;
	sqlite_int64 album_id = -1;
	sqlite3_stmt *stmt;
	time_t now;
	sqlite_int64 _id = -1;
	gboolean pending = FALSE;
	gboolean found;
	gboolean ret;
	int rc;

	/* Check if we can work with this type of file. */
	if (is_supported(path) == FALSE)
		return FALSE;

	/* Check if this file has already been scanned before. */
	rc = sqlite3_prepare(dbh, "SELECT _id, mtime, pending_delete FROM songs WHERE filename = ?",
	  -1, &stmt, NULL);
	assert(rc == SQLITE_OK);

	sqlite3_bind_text(stmt, 1, path, -1, NULL);

	/* It has; check to see if the mtime on disk matches the last recorded
	 * time in the database. */
	if ((rc = sqlite3_step(stmt)) == SQLITE_ROW)
	{
		time_t then;

		_id = sqlite3_column_int64(stmt, 0);
		assert(_id >= 0);

		then = (time_t)sqlite3_column_int64(stmt, 1);
		pending = (sqlite3_column_int(stmt, 2) == 0) ? FALSE : TRUE;

		/* Typical case, no data needs to be updated. */
		if (pending == 0 && then == stbuf->st_mtime)
		{
			sqlite3_finalize(stmt);

			/* "Tick" the file, so that we know not to delete it when the
			 * scan is complete. */
			rc = sqlite3_prepare(dbh, "UPDATE songs SET tick = 1 WHERE filename = ?",
			  -1, &stmt, NULL);
			assert(rc == SQLITE_OK);

			sqlite3_bind_text(stmt, 1, path, -1, NULL);

			rc = sqlite3_step(stmt);
			assert(rc == SQLITE_DONE);

			sqlite3_finalize(stmt);

			return TRUE;
		}
	}

	sqlite3_finalize(stmt);

	/* This file is either new or has been updated since the last time we
	 * scanned.  Open the meta data to update records. */
	if ((tfile = taglib_file_new(path)) == NULL)
		return FALSE;

	//g_message("%s...", path);

	/* This apparently is possible, though I don't understand why... */
	if ((ttag = taglib_file_tag(tfile)) == NULL)
	{
		taglib_file_free(tfile);
		return FALSE;
	}

	taudio = taglib_file_audioproperties(tfile);
	assert(taudio != NULL);

	if ((length = taglib_audioproperties_length(taudio)) <= 0)
	{
		taglib_file_free(tfile);
		return FALSE;
	}

	/* Artist is required to insert any song data. */
	artist = taglib_tag_artist(ttag);

	if (is_empty_tag(artist) == TRUE)
	{
		free(artist);
		taglib_file_free(tfile);
		return FALSE;
	}

	/* Synchronized time to reflect the addition of artist, album, and song. */
	now = time(NULL);

	/* Lookup or insert artist. */
	found = get_artist(dbh, g_strstrip(artist), &artist_id, now);

	if (found == FALSE)
	{
		taglib_file_free(tfile);
		free(artist);
		return FALSE;
	}

	/* Lookup or insert album, if specified. */
	album = taglib_tag_album(ttag);

	if (is_empty_tag(album) == FALSE)
	{
		found = get_album(dbh, g_strstrip(album), artist, artist_id,
		  &album_id, now);

		free(album);
		free(artist);

		if (found == FALSE)
		{
			taglib_file_free(tfile);
			return FALSE;
		}
	}
	else
	{
		free(album);
		free(artist);
	}

	/* Title is a required field.  We could do some "Unknown 1, 2, 3..." type
	 * trick here, but I don't see the need. */
	if ((title = taglib_tag_title(ttag)) == NULL)
	{
		taglib_file_free(tfile);
		return FALSE;
	}

	track = taglib_tag_track(ttag);

	ret = insert_or_replace_song(dbh, _id, pending, path, stbuf,
	  artist_id, album_id, g_strstrip(title), length, track, now);

	g_free(title);
	taglib_file_free(tfile);

	return ret;
}

static void process_work_queue_img(sqlite3 *dbh, struct queuedimg *q)
{
	sqlite3_stmt *stmt;
	const char *sql;
	int rc;

	switch (q->type)
	{
		case WORKER_ARTIST:
			sql = "UPDATE artists SET photo_thumb = ? WHERE _id = ?";
			break;

		case WORKER_ALBUM:
			sql = "UPDATE albums SET artwork_thumb = ? WHERE _id = ?";
			break;
	}

	/* Uhm, it would be much more efficient to prepare these
	 * statements ahead of time. */
	rc = sqlite3_prepare(dbh, sql, -1, &stmt, NULL);
	assert(rc == SQLITE_OK);

	sqlite3_bind_blob(stmt, 1, q->data, q->len, SQLITE_STATIC);
	sqlite3_bind_int64(stmt, 2, q->id);

	rc = sqlite3_step(stmt);
	assert(rc == SQLITE_DONE);

	sqlite3_finalize(stmt);

	queuedimg_free(q);
}

static void process_work_queue_mbid(sqlite3 *dbh, struct queuedmbid *q)
{
	sqlite3_stmt *stmt;
	const char *sql;
	int rc;

	switch (q->type)
	{
		case WORKER_ARTIST:
			sql = "UPDATE artists SET mbid = ? WHERE _id = ?";
			break;

		case WORKER_ALBUM:
			sql = "UPDATE albums SET mbid = ? WHERE _id = ?";
			break;
	}

	/* Uhm, it would be much more efficient to prepare these
	 * statements ahead of time. */
	rc = sqlite3_prepare(dbh, sql, -1, &stmt, NULL);
	assert(rc == SQLITE_OK);

	sqlite3_bind_text(stmt, 1, q->mbid, -1, SQLITE_STATIC);
	sqlite3_bind_int64(stmt, 2, q->id);

	rc = sqlite3_step(stmt);
	assert(rc == SQLITE_DONE);

	sqlite3_finalize(stmt);

	queuedmbid_free(q);
}

static void process_work_queue_watch(sqlite3 *dbh, struct queuedwork *qw)
{
	struct queuedwatch *q = qw->work;

	switch (qw->type)
	{
		case QUEUED_WATCH_CREATE_DIR: watch_create_dir_work(dbh, q->path1, FALSE); break;
		case QUEUED_WATCH_MOVE: watch_move_work(dbh, q->path1, q->path2); break;
		case QUEUED_WATCH_MODIFY: watch_modify_work(dbh, q->path1, FALSE); break;
		case QUEUED_WATCH_DELETE: watch_delete_work(dbh, q->path1); break;
		default: abort();
	}

	queuedwatch_free(q);
}

static void process_work_queue(sqlite3 *dbh)
{
	struct queuedwork *q;

	while ((q = g_async_queue_try_pop(workqueue)) != NULL)
	{
		switch (q->type)
		{
			case QUEUED_IMG:
				process_work_queue_img(dbh, q->work);
				break;
			case QUEUED_MBID:
				process_work_queue_mbid(dbh, q->work);
				break;
			case QUEUED_WATCH_CREATE_DIR:
			case QUEUED_WATCH_MOVE:
			case QUEUED_WATCH_MODIFY:
			case QUEUED_WATCH_DELETE:
				process_work_queue_watch(dbh, q);
				break;
			default:
				abort();
		}

		queuedwork_free(q);
	}
}

static unsigned int traverse(sqlite3 *dbh, const char *pathorig,
  const char *path, unsigned int nfiles)
{
	GDir *dirh;
	GError *err = NULL;
	char pathbuf[PATH_MAX];
	const char *fname;
	gsize baselen;

	assert(pathorig != NULL);
	assert(path != NULL);

	if ((dirh = g_dir_open(path, 0, &err)) == NULL)
	{
		g_warning("Failed to scan meta data directory: %s\n", err->message);
		g_error_free(err);

		goto exit;
	}

	baselen = g_strlcpy(pathbuf, path, sizeof(pathbuf));
	assert(baselen > 0);

	if (baselen >= sizeof(pathbuf) - 3)
	{
		g_warning("path '%s' is too long to process\n", path);
		goto exit;
	}

	/* No buffer overflow here since we just checked that there are at least 3
	 * extra bytes available (one for the separator, one for a filename, and
	 * the terminating NUL). */
	if (G_IS_DIR_SEPARATOR(pathbuf[baselen - 1]) == FALSE)
		pathbuf[baselen++] = G_DIR_SEPARATOR;

	while ((fname = g_dir_read_name(dirh)) != NULL)
	{
		struct stat stbuf;
		gsize len;

		process_work_queue(dbh);

		/* XXX: hide dot files. */
		if (fname[0] == '.')
			continue;

		len = g_strlcpy(pathbuf + baselen, fname, sizeof(pathbuf) - baselen);

		if (len >= sizeof(pathbuf) - baselen - 1)
		{
			g_warning("path '%s is too long to process\n", pathbuf);
			continue;
		}

		if (stat(pathbuf, &stbuf) == -1)
		{
			g_warning("stat(%s) failed: %s\n", pathbuf, g_strerror(errno));
			continue;
		}

		if (S_ISDIR(stbuf.st_mode))
		{
#ifdef TRACK_INODES
			struct unique_inode di;

			di.inode = stbuf.st_ino;
			di.dev = stbuf.st_dev;

			if (g_hash_table_lookup(inodes, &di) != NULL)
			{
				g_warning("Ignoring '%s': already traversed this inode",
				  pathbuf);

				continue;
			}

			g_hash_table_insert(inodes, g_memdup(&di, sizeof(di)),
			  GINT_TO_POINTER(TRUE));
#endif /* TRACK_INODES */
			nfiles = traverse(dbh, pathorig, pathbuf, nfiles);
		}
		else if (S_ISREG(stbuf.st_mode))
		{
			if (handle_file(dbh, pathbuf, &stbuf) == TRUE)
				nfiles++;
		}
	}

	g_dir_close(dirh);

#ifdef USE_INOTIFY
	my_inotify_add_watch(inotify, path);
#endif /* USE_INOTIFY */

exit:
	return nfiles;
}

#ifdef TRACK_INODES
static guint unique_inode_hash(gconstpointer key)
{
	const unsigned char *data = key;
	size_t len = sizeof(struct unique_inode);
	guint hash;
	int i;

	for (i = 0; i < len; i++)
		hash = (hash * 33) + *data++;

	hash = hash + (hash >> 5);

	return hash;
}

static gboolean unique_inode_equal(gconstpointer a, gconstpointer b)
{
	if (memcmp(a, b, sizeof(struct unique_inode)) == 0)
		return TRUE;

	return FALSE;
}

static void unique_inode_destroy(gpointer data)
{
	g_free(data);
}
#endif /* TRACK_INODES */

static void meta_scan_init_one(sqlite3 *dbh)
{
	http_cli = soup_session_sync_new();
	assert(http_cli != NULL);

	/* TODO: Better error reporting. */
	workers = g_thread_pool_new(download_artwork, NULL, 10, FALSE, NULL);
	assert(workers != NULL);

	workqueue = g_async_queue_new();
	assert(workqueue != NULL);

	MagickWandGenesis();
}

static void meta_scan_init(sqlite3 *dbh)
{
	int rc;

#ifdef TRACK_INODES
	inodes = g_hash_table_new_full(unique_inode_hash, unique_inode_equal,
	  unique_inode_destroy, NULL);
	assert(inodes != NULL);
#endif /* TRACK_INODES */

#ifdef USE_INOTIFY
	/* TODO: Reinitialize inotify so that we can start with fresh watches. */
#endif /* USE_INOTIFY */

	rc = sqlite3_exec(dbh, "BEGIN TRANSACTION", NULL, NULL, NULL);
	assert(rc == SQLITE_OK);

	/* Untick all songs; tick them back when they are scanned.  This is
	 * how we detect deletes across invocations. */
	rc = sqlite3_exec(dbh, "UPDATE songs SET tick = 0 WHERE pending_delete = 0", NULL, NULL, NULL);
	assert(rc == SQLITE_OK);

	meta_scan_init_one(dbh);
}

static void meta_scan_finish_one(sqlite3 *dbh)
{
	/* Finish downloads before we commit to the database. */
	g_thread_pool_free(workers, FALSE, TRUE);

	process_work_queue(dbh);
	g_async_queue_unref(workqueue);

	soup_session_abort(http_cli);
	printf("ref=%d\n", http_cli->parent.ref_count);
	g_object_unref(http_cli);

	MagickWandTerminus();
}

static void meta_scan_finish(sqlite3 *dbh, gboolean withdelete)
{
	int rc;

	meta_scan_finish_one(dbh);

	if (withdelete == TRUE)
	{
		rc = sqlite3_exec(dbh, "UPDATE songs SET pending_delete = 1 WHERE tick = 0 AND pending_delete = 0", NULL, NULL, NULL);
		assert(rc == SQLITE_OK);

		rc = sqlite3_exec(dbh, "UPDATE albums SET pending_delete = 1 WHERE _id IN (SELECT albums._id FROM albums LEFT JOIN songs ON songs.album_id = albums._id AND songs.pending_delete = 0 WHERE songs._id IS NULL) AND pending_delete = 0", NULL, NULL, NULL);
		assert(rc == SQLITE_OK);

		rc = sqlite3_exec(dbh, "UPDATE artists SET pending_delete = 1 WHERE _id IN (SELECT artists._id FROM artists LEFT JOIN songs ON songs.artist_id = artists._id AND songs.pending_delete = 0 WHERE songs._id IS NULL) AND pending_delete = 0", NULL, NULL, NULL);
		assert(rc == SQLITE_OK);
	}

	rc = sqlite3_exec(dbh, "COMMIT TRANSACTION", NULL, NULL, NULL);
	assert(rc == SQLITE_OK);

#ifdef TRACK_INODES
	g_hash_table_destroy(inodes);
	inodes = NULL;
#endif /* TRACK_INODES */
}

static gpointer run_crawl_thread(gpointer udata)
{
	sqlite3 *dbh;
	gchar **paths;
	gchar **path;
	unsigned int files = 0;
	int rc;

	g_static_mutex_lock(&crawling);

	if (db_open_again(&dbh) == FALSE)
		g_critical("Database open failed, aborting crawl...");
	else
	{
		GSList *path;

		meta_scan_init(dbh);

		for (path = watch_folders; path != NULL;
		     path = g_slist_next(watch_folders))
		{
			files += traverse(dbh, path->data, path->data, 0);
		}

		meta_scan_finish(dbh, TRUE);

		g_message("scanned %u files", files);

		db_close_again(dbh);
	}

	g_static_mutex_unlock(&crawling);

	return NULL;
}

void meta_crawl(void)
{
	GThread *t;
	GError *err = NULL;

	if (g_static_mutex_trylock(&crawling) == FALSE)
	{
		g_print("Uhm, already scanning...\n");
		return;
	}

	if (watch_folders == NULL)
	{
		g_warning("meta_crawl: Nothing to do!\n");
		return;
	}

	g_message("meta_crawl: Here we go!");

	t = g_thread_create(run_crawl_thread, NULL, FALSE, &err);
	if (t == NULL)
	{
		g_critical("g_thread_create(run_crawl_thread) failed: %s\n", err->message);
		g_error_free(err);
	}

	g_static_mutex_unlock(&crawling);
}

/*****************************************************************************/

static void watch_create_dir_work(sqlite3 *dbh, const char *path,
  gboolean needinit)
{
	unsigned int n;

	/* This function may be called in the crawler thread or in the main thread.
	 * When in the main thread, we need to make sure to reinitialize the
	 * meta_scan features so that handle_file can work. */
	if (needinit == TRUE)
		meta_scan_init(dbh);

	n = traverse(dbh, path, path, 0);

	if (needinit == TRUE)
		meta_scan_finish(dbh, FALSE);

	g_message("added %u files", n);
}

static void watch_move_work(sqlite3 *dbh, const char *src, const char *dst)
{
	sqlite3_stmt *stmt;
	int rc;

	assert(src != NULL);
	assert(dst != NULL);

	g_print("Moving %s to %s...\n", src, dst);

	rc = sqlite3_prepare(dbh, "UPDATE songs SET filename = ? WHERE filename = ?", -1, &stmt, NULL);
	assert(rc == SQLITE_OK);

	sqlite3_bind_text(stmt, 1, dst, -1, SQLITE_STATIC);
	sqlite3_bind_text(stmt, 2, src, -1, SQLITE_STATIC);

	rc = sqlite3_step(stmt);
	assert(rc == SQLITE_DONE);

	sqlite3_finalize(stmt);
}

static void watch_modify_work(sqlite3 *dbh, const char *path, gboolean needinit)
{
	struct stat stbuf;

	g_print("Created or modified file %s...\n", path);

	if (stat(path, &stbuf) == -1)
	{
		g_warning("stat(%s) failed: %s\n", path, g_strerror(errno));
		return;
	}

	if (S_ISREG(stbuf.st_mode) == 0)
		return;

	/* XXX: This check is in handle_file as well, but we moved it out to
	 * possibly avoid meta_scan_init_one() when not necessary. */
	if (is_supported(path) == FALSE)
		return;

	/* This function may be called in the crawler thread or in the main thread.
	 * When in the main thread, we need to make sure to reinitialize the
	 * meta_scan features so that handle_file can work. */
	if (needinit == TRUE)
		meta_scan_init_one(dbh);

	handle_file(dbh, path, &stbuf);

	if (needinit == TRUE)
		meta_scan_finish_one(dbh);
}

static int sql_count_where_id(sqlite3 *dbh, const char *sql, sqlite_int64 id)
{
	sqlite3_stmt *stmt;
	int rc;
	int count;

	rc = sqlite3_prepare(dbh, sql, -1, &stmt, NULL);
	assert(rc == SQLITE_OK);

	sqlite3_bind_int64(stmt, 1, id);

	rc = sqlite3_step(stmt);
	assert(rc == SQLITE_ROW);

	count = sqlite3_column_int(stmt, 0);

	sqlite3_finalize(stmt);

	return count;
}

static void sql_update_where_id(sqlite3 *dbh, const char *sql, sqlite_int64 id)
{
	sqlite3_stmt *stmt;
	int rc;

	rc = sqlite3_prepare(dbh, sql, -1, &stmt, NULL);
	assert(rc == SQLITE_OK);

	sqlite3_bind_int64(stmt, 1, id);

	rc = sqlite3_step(stmt);
	assert(rc == SQLITE_DONE);

	sqlite3_finalize(stmt);
}

static void watch_delete_work(sqlite3 *dbh, const char *path)
{
	sqlite3_stmt *stmt;
	int rc;
	int pending;
	sqlite_int64 id;
	gboolean with_album;
	sqlite_int64 album_id;
	sqlite_int64 artist_id;
	int songs_by_album;
	int songs_by_artist;

	g_print("Deleted file %s...\n", path);

	rc = sqlite3_prepare(dbh, "SELECT _id, pending_delete, album_id, artist_id FROM songs WHERE filename = ?", -1, &stmt, NULL);
	assert(rc == SQLITE_OK);

	sqlite3_bind_text(stmt, 1, path, -1, SQLITE_STATIC);

	rc = sqlite3_step(stmt);

	if (rc == SQLITE_ROW)
	{
		id = sqlite3_column_int(stmt, 0);
		pending = sqlite3_column_int(stmt, 1);

		if (sqlite3_column_type(stmt, 2) != SQLITE_NULL)
		{
			album_id = sqlite3_column_int64(stmt, 2);
			with_album = TRUE;
		}
		else
		{
			album_id = -1;
			with_album = FALSE;
		}

		artist_id = sqlite3_column_int64(stmt, 3);
	}

	sqlite3_finalize(stmt);

	if (rc != SQLITE_ROW || pending)
	{
		g_warning("File %s not indexed, so delete is ignored...", path);
		return;
	}

	/* Mark this song for deletion. */
	rc = sqlite3_prepare(dbh, "UPDATE songs SET pending_delete = 1 WHERE _id = ?", -1, &stmt, NULL);
	assert(rc == SQLITE_OK);

	sqlite3_bind_int64(stmt, 1, id);

	rc = sqlite3_step(stmt);
	assert(rc == SQLITE_DONE);

	sqlite3_finalize(stmt);

	if (with_album == TRUE)
	{
		/* Check if we must also mark the album for deletion. */
		songs_by_album = sql_count_where_id(dbh, "SELECT COUNT(*) FROM songs WHERE pending_delete = 0 AND album_id = ?", album_id);

		if (songs_by_album == 0)
		{
			g_print("Album %d exhausted...\n", album_id);

			/* TODO: Assert that pending_delete is not already TRUE. */
			sql_update_where_id(dbh, "UPDATE albums SET pending_delete = 1 WHERE _id = ?", album_id);
		}
	}

	/* Check if we must also mark the artist for deletion. */
	songs_by_artist = sql_count_where_id(dbh, "SELECT COUNT(*) FROM songs WHERE pending_delete = 0 AND artist_id = ?", artist_id);

	if (songs_by_artist == 0)
	{
		g_print("Artist %d exhausted...\n", artist_id);

		/* TODO: Assert that pending_delete is not already TRUE. */
		sql_update_where_id(dbh, "UPDATE artists SET pending_delete = 1 WHERE _id = ?", artist_id);
	}
}

static void watch_do_or_queue(enum queuedwork_type type,
  const char *path1, const char *path2)
{
	if (g_static_mutex_trylock(&crawling) == FALSE)
		g_async_queue_push(workqueue, queuedwatch_new(type, path1, path2));
	else
	{
		switch (type)
		{
			case QUEUED_WATCH_CREATE_DIR: watch_create_dir_work(db, path1, TRUE); break;
			case QUEUED_WATCH_MOVE: watch_move_work(db, path1, path2); break;
			case QUEUED_WATCH_MODIFY: watch_modify_work(db, path1, TRUE); break;
			case QUEUED_WATCH_DELETE: watch_delete_work(db, path1); break;
			default: abort();
		}

		g_static_mutex_unlock(&crawling);
	}
}

static void watch_create_dir_cb(const char *path)
{
	watch_do_or_queue(QUEUED_WATCH_CREATE_DIR, path, NULL);
}

static void watch_move_cb(const char *src, const char *dst)
{
	watch_do_or_queue(QUEUED_WATCH_MOVE, src, dst);
}

static void watch_modify_cb(const char *path)
{
	watch_do_or_queue(QUEUED_WATCH_MODIFY, path, NULL);
}

static void watch_delete_cb(const char *path)
{
	watch_do_or_queue(QUEUED_WATCH_DELETE, path, NULL);
}
